/*
 * Copyright 2011 The Rusted Roof, LLC.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package org.proteusframework.core.base;

import org.proteusframework.core.api.config.IConfigProperty;
import org.proteusframework.core.api.config.IConfigPropertyPrototype;
import org.proteusframework.core.api.config.NoSuchConfigurationException;
import org.proteusframework.core.api.model.IDisplayableName;
import org.proteusframework.core.api.model.IStandardPlugin;
import org.proteusframework.core.util.Assert;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;


/**
 * Base class for all {@link IStandardPlugin} objects.
 *
 * @author Tacoma Four
 */
public class AbstractStandardPlugin extends AbstractPlugin implements IStandardPlugin, InvocationHandler
{
    /**
     * Class that implements the given standard plugin.
     */
    private final Class clazz;

    /**
     * Collection of runtime-defined configuration properties.
     */
    private Map<String, IConfigProperty> configuredProperties = new HashMap<String, IConfigProperty>();

    /**
     * Collection of supported configurable property prototypes.
     */
    private Collection<IConfigPropertyPrototype> configuredPropertyPrototypes = new ArrayList<IConfigPropertyPrototype>();

    /**
     * Displayable name information.
     */
    private IDisplayableName displayableName;

    /**
     * Constructs an abstract plugin object associated with the immutable <code>INamespace</code> that
     * is concretely instantiated with the named class.
     *
     * @param displayableName Displayable name
     * @param clazz           A class that must implement an InvocationHandler
     */
    protected AbstractStandardPlugin(final IDisplayableName displayableName, final Class clazz)
    {
        super(displayableName);
        this.displayableName = displayableName;
        this.clazz = clazz;
    }

    protected final void addConfigurationPropertyPrototype(IConfigPropertyPrototype prototype)
    {
        Assert.parameterNotNull(prototype, "Parameter 'prototype' must not be null");
        configuredPropertyPrototypes.add(prototype);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final String getName()
    {
        return displayableName.getName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final String getDescription()
    {
        return displayableName.getDescription();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final URL getGlyphURL()
    {
        return displayableName.getGlyphURL();
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public final Object invoke(Object o, Method method, Object[] objects) throws Throwable
    {
        Method match = clazz.getMethod(method.getName(), method.getParameterTypes());
        return match.invoke(this, objects);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final void processRawProperty(IConfigProperty property)
    {
        onRawConfigurationPropertySet(property);
    }

    /**
     * Event that provides notice that an internal property was defined at runtime. This event gives the implementor
     * the opportunity to inspect and respond to the newly defined runtime configuration property.
     *
     * @param property property defined
     * @return true, if the configuration property should be accepted, or false if it should be silently rejected. If
     *         rejected, it is the responsibility of the concrete class to adequately log the rejection, and the
     *         concrete class is strongly encouraged to throw an <code>IllegalArgumentException</code> to ensure that
     *         the rejection is noticed.
     */
    protected boolean onConfigurationPropertySet(IConfigProperty property)
    {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<IConfigProperty> configuredProperties()
    {
        return Collections.unmodifiableCollection(configuredProperties.values());
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public final Collection<IConfigPropertyPrototype> configurableProperties()
    {
        return Collections.unmodifiableCollection(configuredPropertyPrototypes);
    }

    /**
     * Event that provides notice that an internal property was defined at runtime from an XML configuration. This event
     * gives the implementor the opportunity to inspect and respond to the newly defined runtime configuration property
     * by properly parsing and converting it, as applicable, to the correct type of object.
     *
     * @param property property defined
     */
    protected void onRawConfigurationPropertySet(IConfigProperty property)
    {

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final IConfigProperty getConfigProperty(String key)
    {
        return configuredProperties.get(key);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final void setConfigProperty(IConfigProperty property) throws IllegalArgumentException
    {
        Assert.parameterNotNull(property, "Parameter 'property' must not be null");

        boolean propertyExists = false;

        for (IConfigPropertyPrototype prototype : configuredPropertyPrototypes)
        {
            if (prototype.getKey().equals(property.getKey()))
            {
                // match
                propertyExists = true;
                break;
            }
        }

        if (!propertyExists)
        {
            throw new IllegalArgumentException("The property '" + property.getKey() + "' is not defined within the list of configuration property prototypes");
        }

        boolean acceptValue = onConfigurationPropertySet(property);

        if (acceptValue)
        {
            configuredProperties.put(property.getKey(), property);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final Object getConfigPropertyOrDefault(String key) throws NoSuchConfigurationException
    {
        Object assignedValue = null;

        if (configuredProperties.containsKey(key))
        {
            IConfigProperty configProperty = configuredProperties.get(key);
            assignedValue = configProperty.getAssignedValue();
        } else
        {
            IConfigPropertyPrototype matchedPrototype = null;

            for (IConfigPropertyPrototype prototype : configuredPropertyPrototypes)
            {
                if (prototype.getKey().equals(key))
                {
                    // match
                    matchedPrototype = prototype;
                    break;
                }
            }

            if (null == matchedPrototype)
            {
                throw new IllegalArgumentException("The property '" + key + "' is not defined within the list of configuration property prototypes");
            } else
            {
                assignedValue = matchedPrototype.getDefaultValue();
            }
        }
        return assignedValue;
    }
}
